/*
 Copyright (c) 2012, 2021, Oracle and/or its affiliates. All rights
 reserved.
 
 This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License, version 2.0,
 as published by the Free Software Foundation.

 This program is also distributed with certain software (including
 but not limited to OpenSSL) that is licensed under separate terms,
 as designated in a particular file or component or in included license
 documentation.  The authors of MySQL hereby grant you an additional
 permission to link the program and your derivative works with the
 separately licensed software that they have included with MySQL.

 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License, version 2.0, for more details.

 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 02110-1301  USA
 */

"use strict";

var     udebug     = unified_debug.getLogger("Transaction.js");
var userContext    = require("./UserContext.js");

/** Transaction is implemented as a state machine. 
 * States are:
 * idle: user has not called begin
 * active user has called begin
 * rollback_only user has called begin and setRollbackOnly
 * 
 * Functions that cause state transitions are:
 * begin: begin a transaction if not already begun
 * rollback: rollback the current transaction
 * setRollbackOnly: allow rollback but not commit
 * commit: commit the current transaction
 * rollback: roll back the current transaction
 * 
 * Functions that return status are:
 * getRollbackOnly returns whether the state is rollback only
 * isActive returns whether the state is active
 * 
 * There are only three valid transitions:
 * 
 * (Idle) begin -> (Active) commit -> (Idle)
 * (Idle) begin -> (Active) rollback -> (Idle)
 * (Idle) begin -> (Active) setRollbackOnly -> (RollbackOnly) rollback -> (Idle)
 */

function Idle() {
  this.name = 'Idle';
}
var idle = new Idle();

function Active() {
  this.name = 'Active';
}
var active = new Active();

function RollbackOnly() {
  this.name = 'RollbackOnly';
}
var rollbackOnly = new RollbackOnly();

/** An error may have occurred. If there is a callback defined, signal the error via the callback,
 * and return the new transaction state. The state should remain the same (the current state
 * is passed in the function).
 * If no callback is defined with an error, throw the error (and remain in the current state).
 */
var callbackErrOrThrow = function(err, user_arguments) {
  var promise = new userContext.Promise();
  // signal the error via the promise
  if (err) {
    promise.reject(err);
  } else {
    promise.fulfill();
  }
   if (typeof(user_arguments[0]) === 'function') {
    var return_arguments = [];
    var i;
    for (i = 1; i < user_arguments.length; ++i) {
      return_arguments[i] = user_arguments[i];
    }
    return_arguments[0] = err;
    user_arguments[0].apply(null, return_arguments);
  }
 return promise;
};

Idle.prototype.begin = function(session, user_arguments) {
  udebug.log('Idle begin');
  // notify dbSession if they are interested
  if (typeof(session.dbSession.begin) === 'function') {
    session.dbSession.begin();
  }
  session.tx.setState(active);
  // no error
  return callbackErrOrThrow(null, user_arguments);
};

Idle.prototype.commit = function(session, user_arguments) {
  udebug.log('Idle commit');
  var err = new Error('Illegal state: Idle cannot commit.');
  err.sqlstate = '25000';
  return callbackErrOrThrow(err, user_arguments);
};

Idle.prototype.rollback = function(session, user_arguments) {
  udebug.log('Idle rollback');
  var err = new Error('Illegal state: Idle cannot rollback.');
  err.sqlstate = '25000';
  return callbackErrOrThrow(err, user_arguments);
};

Idle.prototype.isActive = function() {
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('Idle isActive');
  return false;
};

Idle.prototype.setRollbackOnly = function(session, user_arguments) {
  udebug.log('Idle setRollbackOnly');
};

Idle.prototype.getRollbackOnly = function() {
  udebug.log('Idle getRollbackOnly');
  return false;
};

Active.prototype.begin = function(session, user_arguments) {
  udebug.log('Active begin');
  var err = new Error('Illegal state: Active cannot begin.');
  err.sqlstate = '25000';
  return callbackErrOrThrow(err, user_arguments);
};

Active.prototype.commit = function(session, user_arguments) {
  udebug.log('Active commit');
  var context = new userContext.UserContext(user_arguments, 1, 1, session, session.sessionFactory);
  // delegate to context's commit for execution which will change the state to idle
  return context.commit();
};

Active.prototype.rollback = function(session, user_arguments) {
  udebug.log('Active rollback');
  var context = new userContext.UserContext(user_arguments, 1, 1, session, session.sessionFactory);
  // delegate to context's rollback for execution which will change the state to idle
  return context.rollback();
};

Active.prototype.isActive = function() {
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('Active isActive');
  return true;
};

Active.prototype.setRollbackOnly = function(session) {
  udebug.log('Active setRollbackOnly');
  session.tx.setState(rollbackOnly);
};

Active.prototype.getRollbackOnly = function() {
  udebug.log('Active getRollbackOnly');
  return false;
};

RollbackOnly.prototype.begin = function(session, user_arguments) {
  udebug.log('RollbackOnly begin');
  var err = new Error('Illegal state: RollbackOnly cannot begin.');
  err.sqlstate = '25000';
  return callbackErrOrThrow(err, user_arguments);
};

RollbackOnly.prototype.commit = function(session, user_arguments) {
  udebug.log('RollbackOnly commit');
  var err = new Error('Illegal state: RollbackOnly cannot commit.');
  err.sqlstate = '25000';
  return callbackErrOrThrow(err, user_arguments);
};

RollbackOnly.prototype.rollback = function(session, user_arguments) {
  udebug.log('RollbackOnly rollback');
  var context = new userContext.UserContext(user_arguments, 1, 1, session, session.sessionFactory);
  // delegate to context's rollback for execution which will set the state to idle
  return context.rollback();
};

RollbackOnly.prototype.isActive = function() {
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('RollbackOnly isActive');
  return true;
};

RollbackOnly.prototype.setRollbackOnly = function() {
  udebug.log('RollbackOnly setRollbackOnly');
};

RollbackOnly.prototype.getRollbackOnly = function() {
  udebug.log('RollbackOnly getRollbackOnly');
  return true;
};

function Transaction(session) {
  this.session = session;
  this.state = idle;
}

// States can be addressed by name in the Transaction object for setState(newState)
Transaction.prototype.idle = idle;
Transaction.prototype.active = active;
Transaction.prototype.rollbackOnly = rollbackOnly;

Transaction.prototype.begin = function() {
  udebug.log('Transaction.begin');
  return this.state.begin(this.session, arguments);
};

Transaction.prototype.commit = function() {
  udebug.log('Transaction.commit');
  return this.state.commit(this.session, arguments);
};

Transaction.prototype.rollback = function() {
  udebug.log('Transaction.rollback');
  return this.state.rollback(this.session, arguments);
};

Transaction.prototype.isActive = function() {
  if(udebug.is_detail()) if(udebug.is_debug()) udebug.log('Transaction.isActive');
  return this.state.isActive();
};

Transaction.prototype.setRollbackOnly = function() {
  udebug.log('Transaction.setRollbackOnly');
  this.state.setRollbackOnly(this.session, arguments);
};

Transaction.prototype.getRollbackOnly = function() {
  udebug.log('Transaction.getRollbackOnly');
  return this.state.getRollbackOnly();
};

/** This function is used by each state to change the state of the transaction.
 * @param newState one of: Idle, Active, RollbackOnly
 */
Transaction.prototype.setState = function(newState) {
  udebug.log('Transaction.setState to', newState.name);
  this.state = newState;
};

exports.Transaction = Transaction;

